package org.ovirt.engine.core.bll.storage.disk;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.ovirt.engine.core.bll.CommandBase;
import org.ovirt.engine.core.bll.LockMessage;
import org.ovirt.engine.core.bll.LockMessagesMatchUtil;
import org.ovirt.engine.core.bll.NonTransactiveCommandAttribute;
import org.ovirt.engine.core.bll.context.CommandContext;
import org.ovirt.engine.core.bll.storage.disk.image.ImagesHandler;
import org.ovirt.engine.core.bll.utils.PermissionSubject;
import org.ovirt.engine.core.bll.validator.storage.DiskImagesValidator;
import org.ovirt.engine.core.bll.validator.storage.StoragePoolValidator;
import org.ovirt.engine.core.common.AuditLogType;
import org.ovirt.engine.core.common.VdcObjectType;
import org.ovirt.engine.core.common.action.GetDiskAlignmentParameters;
import org.ovirt.engine.core.common.action.LockProperties;
import org.ovirt.engine.core.common.action.LockProperties.Scope;
import org.ovirt.engine.core.common.businessentities.ActionGroup;
import org.ovirt.engine.core.common.businessentities.ArchitectureType;
import org.ovirt.engine.core.common.businessentities.StorageDomainStatic;
import org.ovirt.engine.core.common.businessentities.StoragePool;
import org.ovirt.engine.core.common.businessentities.VDS;
import org.ovirt.engine.core.common.businessentities.VDSStatus;
import org.ovirt.engine.core.common.businessentities.VM;
import org.ovirt.engine.core.common.businessentities.storage.Disk;
import org.ovirt.engine.core.common.businessentities.storage.DiskAlignment;
import org.ovirt.engine.core.common.businessentities.storage.DiskImage;
import org.ovirt.engine.core.common.businessentities.storage.DiskStorageType;
import org.ovirt.engine.core.common.businessentities.storage.ImageStatus;
import org.ovirt.engine.core.common.businessentities.storage.LunDisk;
import org.ovirt.engine.core.common.businessentities.storage.VolumeFormat;
import org.ovirt.engine.core.common.errors.EngineError;
import org.ovirt.engine.core.common.errors.EngineException;
import org.ovirt.engine.core.common.errors.EngineMessage;
import org.ovirt.engine.core.common.locks.LockingGroup;
import org.ovirt.engine.core.common.utils.Pair;
import org.ovirt.engine.core.common.vdscommands.GetDiskAlignmentVDSCommandParameters;
import org.ovirt.engine.core.common.vdscommands.GetDiskImageAlignmentVDSCommandParameters;
import org.ovirt.engine.core.common.vdscommands.GetDiskLunAlignmentVDSCommandParameters;
import org.ovirt.engine.core.common.vdscommands.VDSCommandType;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.dal.dbbroker.auditloghandling.AuditLogDirector;
import org.ovirt.engine.core.dao.BaseDiskDao;
import org.ovirt.engine.core.dao.DiskDao;
import org.ovirt.engine.core.dao.StorageDomainStaticDao;
import org.ovirt.engine.core.dao.StoragePoolDao;
import org.ovirt.engine.core.dao.VdsDao;
import org.ovirt.engine.core.dao.VmDao;
import org.ovirt.engine.core.utils.transaction.TransactionSupport;

@NonTransactiveCommandAttribute(forceCompensation = true)
public class GetDiskAlignmentCommand<T extends GetDiskAlignmentParameters> extends CommandBase<T> {

    @Inject
    private AuditLogDirector auditLogDirector;

    @Inject
    private StorageDomainStaticDao storageDomainStaticDao;
    @Inject
    private StoragePoolDao storagePoolDao;
    @Inject
    private BaseDiskDao baseDiskDao;
    @Inject
    private VdsDao vdsDao;
    @Inject
    private DiskDao diskDao;
    @Inject
    private VmDao vmDao;

    private Disk diskToScan;
    private Guid vdsInPool;
    private Guid storagePoolId;
    private VM diskVm;
    private List<PermissionSubject> permsList;

    @Inject
    private ImagesHandler imagesHandler;

    public GetDiskAlignmentCommand(T parameters, CommandContext cmdContext) {
        super(parameters, cmdContext);
    }

    public GetDiskAlignmentCommand(Guid commandId) {
        super(commandId);
    }

    @Override
    protected LockProperties applyLockProperties(LockProperties lockProperties) {
        return lockProperties.withScope(Scope.Execution);
    }

    @Override
    protected Map<String, Pair<String, String>> getExclusiveLocks() {
        if (getDisk() != null && isImageExclusiveLockNeeded()) {
            return Collections.singletonMap(getDisk().getId().toString(),
                    LockMessagesMatchUtil.makeLockingPair(LockingGroup.DISK, getDiskIsUsedByGetAlignment()));
        }
        return null;
    }

    @Override
    protected Map<String, Pair<String, String>> getSharedLocks() {
        if (getDisk() != null && !isImageExclusiveLockNeeded()) {
            return Collections.singletonMap(getDisk().getId().toString(),
                    LockMessagesMatchUtil.makeLockingPair(LockingGroup.DISK, getDiskIsUsedByGetAlignment()));
        }
        return null;
    }

    private LockMessage getDiskIsUsedByGetAlignment() {
        return new LockMessage(EngineMessage.ACTION_TYPE_FAILED_DISK_IS_USED_BY_GET_ALIGNMENT)
                .with("DiskAlias", getDiskAlias());
    }

    @Override
    protected void setActionMessageParameters() {
        addValidationMessage(EngineMessage.VAR__ACTION__SCAN_ALIGNMENT);
        addValidationMessage(EngineMessage.VAR__TYPE__DISK);
    }

    @Override
    public Map<String, String> getJobMessageProperties() {
        if (jobProperties == null) {
            jobProperties = super.getJobMessageProperties();
            jobProperties.put("diskalias", getDiskAlias());
        }
        return jobProperties;
    }

    @Override
    protected boolean validate() {
        if (getDisk() == null) {
            return failValidation(EngineMessage.ACTION_TYPE_FAILED_DISK_NOT_EXIST);
        }

        if (getVm() == null) {
            addValidationMessageVariable("diskAliases", getDiskAlias());
            return failValidation(EngineMessage.ACTION_TYPE_FAILED_DISK_IS_NOT_VM_DISK);
        }

        if (ArchitectureType.ppc == getCluster().getArchitecture().getFamily()) {
            return failValidation(EngineMessage.ACTION_TYPE_FAILED_ALIGNMENT_SCAN_NOT_SUPPORTED_ON_PPC);
        }

        if (getDiskStorageType() == DiskStorageType.IMAGE) {
            DiskImagesValidator diskImagesValidator = new DiskImagesValidator((DiskImage) getDisk());
            if (!validate(diskImagesValidator.diskImagesNotLocked()) ||
                    !validate(diskImagesValidator.diskImagesNotIllegal())) {
                return false;
            }

            StorageDomainStatic sds = storageDomainStaticDao.get(((DiskImage) getDisk()).getStorageIds().get(0));
            if (!sds.getStorageType().isBlockDomain()) {
                addValidationMessageVariable("diskAlias", getDiskAlias());
                return failValidation(EngineMessage.ACTION_TYPE_FAILED_ALIGNMENT_SCAN_STORAGE_TYPE);
            }
        }

        if (getVm().isRunningOrPaused()) {
            return failValidation(EngineMessage.ERROR_CANNOT_RUN_ALIGNMENT_SCAN_VM_IS_RUNNING);
        }

        if (getVdsIdInGroup() == null) {
            return failValidation(EngineMessage.ACTION_TYPE_FAILED_NO_VDS_IN_POOL);
        }

        StoragePool sp = storagePoolDao.get(getStoragePoolId());
        if (!validate(new StoragePoolValidator(sp).existsAndUp())) {
            return false;
        }

        return true;
    }

    @Override
    protected void executeCommand() {
        GetDiskAlignmentVDSCommandParameters parameters;

        auditLogDirector.log(this, AuditLogType.DISK_ALIGNMENT_SCAN_START);

        acquireExclusiveDiskDbLocks();

        // Live scan is not supported yet, this might become: getVm().getId()
        Guid vmId = Guid.Empty;

        if (getDiskStorageType() == DiskStorageType.IMAGE) {
            DiskImage diskImage = (DiskImage) getDisk();

            GetDiskImageAlignmentVDSCommandParameters imageParameters =
                    new GetDiskImageAlignmentVDSCommandParameters(getVdsIdInGroup(), vmId);

            imageParameters.setPoolId(getStoragePoolId());
            imageParameters.setDomainId(diskImage.getStorageIds().get(0));
            imageParameters.setImageGroupId(diskImage.getId());
            imageParameters.setImageId(diskImage.getImageId());

            parameters = imageParameters;
        } else if (getDiskStorageType() == DiskStorageType.LUN) {
            LunDisk lunDisk = (LunDisk) getDisk();

            GetDiskLunAlignmentVDSCommandParameters lunParameters =
                    new GetDiskLunAlignmentVDSCommandParameters(getVdsIdInGroup(), vmId);

            lunParameters.setLunId(lunDisk.getLun().getLUNId());

            parameters = lunParameters;
        } else {
            throw new EngineException(EngineError.ENGINE, "Unknown DiskStorageType: " +
                getDiskStorageType().toString() + " Disk id: " + getDisk().getId().toString());
        }

        Boolean isDiskAligned = (Boolean) runVdsCommand(
                VDSCommandType.GetDiskAlignment, parameters).getReturnValue();

        getDisk().setAlignment(isDiskAligned ? DiskAlignment.Aligned : DiskAlignment.Misaligned);
        getDisk().setLastAlignmentScan(new Date());


        baseDiskDao.update(getDisk());
        setSucceeded(true);

        releaseExclusiveDiskDbLocks();
    }

    @Override
    public List<PermissionSubject> getPermissionCheckSubjects() {
        if (permsList == null && getDisk() != null) {
            permsList = new ArrayList<>();
            permsList.add(new PermissionSubject(getDisk().getId(),
                    VdcObjectType.Disk, ActionGroup.ATTACH_DISK));
            permsList.add(new PermissionSubject(getDisk().getId(),
                    VdcObjectType.Disk, ActionGroup.EDIT_DISK_PROPERTIES));
        }
        return permsList;
    }

    protected void acquireExclusiveDiskDbLocks() {
        if (isImageExclusiveLockNeeded()) {
            final DiskImage diskImage = (DiskImage) getDisk();

            TransactionSupport.executeInNewTransaction(() -> {
                getCompensationContext().snapshotEntityStatus(diskImage.getImage());
                getCompensationContext().stateChanged();
                diskImage.setImageStatus(ImageStatus.LOCKED);
                imagesHandler.updateImageStatus(diskImage.getImageId(), ImageStatus.LOCKED);
                return null;
            });
        }
    }

    protected void releaseExclusiveDiskDbLocks() {
        if (isImageExclusiveLockNeeded()) {
            final DiskImage diskImage = (DiskImage) getDisk();

            diskImage.setImageStatus(ImageStatus.OK);
            imagesHandler.updateImageStatus(diskImage.getImageId(), ImageStatus.OK);
        }
    }

    protected boolean isImageExclusiveLockNeeded() {
        /* In case the volume format is RAW (same as a direct LUN) the exclusive image
         * lock is not needed since the alignment scan can run without any interference
         * by a concurrent running VM.
         */
        return getDiskStorageType() == DiskStorageType.IMAGE &&
                ((DiskImage) getDisk()).getVolumeFormat() != VolumeFormat.RAW;
    }

    @Override
    public Guid getStoragePoolId() {
        if (storagePoolId == null && getVm() != null) {
            storagePoolId = getVm().getStoragePoolId();
        }
        return storagePoolId;
    }

    protected Guid getVdsIdInGroup() {
        if (vdsInPool == null && getCluster() != null) {
            List<VDS> vdsInPoolList = vdsDao.getAllForClusterWithStatus(getCluster().getId(), VDSStatus.Up);
            if (!vdsInPoolList.isEmpty()) {
                vdsInPool = vdsInPoolList.get(0).getId();
            }
        }
        return vdsInPool;
    }

    @Override
    public VM getVm() {
        if (diskVm == null && getDisk() != null) {
            diskVm = vmDao.getVmsListForDisk(getDisk().getId(), false).stream().findFirst().orElse(null);
        }
        return diskVm;
    }

    public String getDiskAlias() {
        if (getDisk() != null) {
            return getDisk().getDiskAlias();
        }
        return "";
    }

    protected DiskStorageType getDiskStorageType() {
        return getDisk().getDiskStorageType();
    }

    protected Disk getDisk() {
        if (diskToScan == null) {
            diskToScan = diskDao.get(getParameters().getDiskId());
        }
        return diskToScan;
    }

    @Override
    public AuditLogType getAuditLogTypeValue() {
        return getSucceeded() ? AuditLogType.DISK_ALIGNMENT_SCAN_SUCCESS : AuditLogType.DISK_ALIGNMENT_SCAN_FAILURE;
    }
}
